﻿//==============================================================================
// File: PropertyManager.cs
// Created: 2010-05-10
// Author: Piotr Włodek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with WpfRcp. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using WpfRcp.PropertyModel.Internal.Common;
using System.Linq.Expressions;
using Res = WpfRcp.PropertyModel.Properties.Resources;

namespace WpfRcp.PropertyModel
{
    /// <summary>
    /// Represents a property manager responsible for registering properties. It can also
    /// provide information regarding all registered static properties (i.e. permanent and
    /// transient) for a particular type.
    /// </summary>
    public static class PropertyManager
    {
        private static readonly Dictionary<Type, MetaPropertyCollection> MetaProperties;
        private static readonly HashSet<Type> InitializedTypes;

        static PropertyManager()
        {
            MetaProperties = new Dictionary<Type, MetaPropertyCollection>();
            InitializedTypes = new HashSet<Type>();
        }

        private static MetaPropertyCollection GetPropertyListCache(Type ownerType)
        {
            MetaPropertyCollection list;

            if (!(MetaProperties.TryGetValue(ownerType, out list)))
            {
                lock (MetaProperties)
                {
                    if (!(MetaProperties.TryGetValue(ownerType, out list)))
                    {
                        list = new MetaPropertyCollection();
                        MetaProperties.Add(ownerType, list);
                    }
                }
            }

            return list;
        }

        private static MetaProperty RegisterStaticProperty(Type ownerType, MetaProperty property)
        {
            var list = GetPropertyListCache(ownerType);
            list.Add(property);

            return property;
        }

        /// <summary>
        /// Gets registered static (i.e. permanent or transient) properties for
        /// a particular type, including all its subtypes.
        /// </summary>
        /// <param name="type">Type for which a list of properties is being returned.</param>
        /// <returns>All registered properties for a given type (including subtypes).</returns>
        public static IEnumerable<MetaProperty> GetRegisteredProperties(Type type)
        {
            // Get inheritance hierarchy
            var hierarchy = new List<Type>();

            do
            {
                hierarchy.Add(type);
                type = type.BaseType;
            } while (type != null && type != typeof(PropertyContainer));

            // Walk from top to bottom to build consolidated list
            for (int index = hierarchy.Count - 1; index >= 0; index--)
            {
                foreach (var metaProperty in GetPropertyListCache(hierarchy[index]))
                {
                    yield return metaProperty;
                }
            }
        }

        /// <summary>
        /// Initializes static properties for a particular type, including all its subtypes.
        /// </summary>
        /// <param name="type">Type for which static properties are being initialized.</param>
        /// <returns>All registered properties for a given type (including subtypes).</returns>
        public static IEnumerable<MetaProperty> InitializeStaticProperties(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");

            if (InitializedTypes.Contains(type))
                return GetRegisteredProperties(type);

            // If not ititialized, perform it now
            ForceStaticFieldInit(type);
            var propertyCollection = GetRegisteredProperties(type);

            // Set Index properties on all unindexed MetaProperty objects
            var max = -1;
            foreach (var item in propertyCollection)
            {
                if (item.Index == -1)
                {
                    max++;
                    item.Index = max;
                }
                else
                {
                    max = item.Index;
                }
            }

            // Return consolidated list
            return propertyCollection;
        }

        /// <summary>
        /// Gets registered static (i.e. permanent or transient) properties for
        /// a particular type.
        /// </summary>
        /// <param name="type">Type for which a list of properties is being returned.</param>
        /// <returns>All registered properties for a given type.</returns>
        public static ICollection<MetaProperty> GetRegisteredPropertiesForOwner(Type type)
        {
            var list = GetPropertyListCache(type);
            return new List<MetaProperty>(list);
        }

        public static MetaProperty Register(string name, Type propertyType, Type ownerType)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, false,
                new PropertyMetadata(), MetaPropertyOptions.None);

            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty Register<TOwner>(Expression<Func<TOwner, object>> property)
        {
            return Register(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner));
        }

        public static MetaProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata metadata)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, false,
                metadata, MetaPropertyOptions.None);

            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty Register<TOwner>(Expression<Func<TOwner, object>> property, PropertyMetadata metadata)
        {
            return Register(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), metadata);
        }

        public static MetaProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata metadata, MetaPropertyOptions options)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, false,
                metadata, options);

            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty Register<TOwner>(Expression<Func<TOwner, object>> property, PropertyMetadata metadata, MetaPropertyOptions options)
        {
            return Register(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), metadata, options);
        }

        public static MetaPropertyKey RegisterReadOnly(string name, Type propertyType, Type ownerType)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, true,
                new PropertyMetadata(), MetaPropertyOptions.None);
            
            RegisterStaticProperty(ownerType, metaProperty);

            return new MetaPropertyKey(metaProperty);
        }

        public static MetaPropertyKey RegisterReadOnly<TOwner>(Expression<Func<TOwner, object>> property)
        {
            return RegisterReadOnly(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner));
        }

        public static MetaPropertyKey RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata metadata)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, true,
                metadata, MetaPropertyOptions.None);

            RegisterStaticProperty(ownerType, metaProperty);

            return new MetaPropertyKey(metaProperty);
        }

        public static MetaPropertyKey RegisterReadOnly<TOwner>(Expression<Func<TOwner, object>> property, PropertyMetadata metadata)
        {
            return RegisterReadOnly(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), metadata);
        }

        public static MetaPropertyKey RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata metadata, MetaPropertyOptions options)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreatePermanent(name, propertyType, ownerType, true,
                metadata, options);

            RegisterStaticProperty(ownerType, metaProperty);

            return new MetaPropertyKey(metaProperty);
        }

        public static MetaPropertyKey RegisterReadOnly<TOwner>(Expression<Func<TOwner, object>> property, PropertyMetadata metadata, MetaPropertyOptions options)
        {
            return RegisterReadOnly(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), metadata, options);
        }

        public static MetaProperty RegisterTransient(string name, Type propertyType, Type ownerType, TransientPropertyCallback transientPropertyCallback)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreateTransient(name, propertyType, ownerType, transientPropertyCallback, new PropertyMetadata(), MetaPropertyOptions.None);
            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty RegisterTransient<TOwner>(Expression<Func<TOwner, object>> property, TransientPropertyCallback transientPropertyCallback)
        {
            return RegisterTransient(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), transientPropertyCallback);
        }

        public static MetaProperty RegisterTransient(string name, Type propertyType, Type ownerType, TransientPropertyCallback transientPropertyCallback, PropertyMetadata metadata)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreateTransient(name, propertyType, ownerType, transientPropertyCallback, metadata, MetaPropertyOptions.None);
            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty RegisterTransient<TOwner>(Expression<Func<TOwner, object>> property, TransientPropertyCallback transientPropertyCallback, PropertyMetadata metadata)
        {
            return RegisterTransient(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), transientPropertyCallback, metadata);
        }

        public static MetaProperty RegisterTransient(string name, Type propertyType, Type ownerType, TransientPropertyCallback transientPropertyCallback, PropertyMetadata metadata, MetaPropertyOptions options, params MetaProperty[] sourceProperties)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            var metaProperty = MetaProperty.CreateTransient(name, propertyType, ownerType, transientPropertyCallback, metadata, options);

            if (sourceProperties != null)
            {
                foreach (var sourceProperty in sourceProperties)
                {
                    if (sourceProperty == null)
                        throw new ArgumentNullException("sourceProperties", Res.ExceptionSourcePropertyNull);

                    sourceProperty.DependentTransientProperties.Add(metaProperty);
                }
            }

            return RegisterStaticProperty(ownerType, metaProperty);
        }

        public static MetaProperty RegisterTransient<TOwner>(Expression<Func<TOwner, object>> property, TransientPropertyCallback transientPropertyCallback, PropertyMetadata metadata, MetaPropertyOptions options, params MetaProperty[] sourceProperties)
        {
            return RegisterTransient(typeof(TOwner).GetPropertyName(property),
                typeof(TOwner).GetPropertyType(property), typeof(TOwner), transientPropertyCallback, metadata, options, sourceProperties);
        }

        public static MetaProperty RegisterAttached(string name, Type propertyType, Type ownerType)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            return MetaProperty.CreateAttached(name, propertyType, ownerType,
                new PropertyMetadata(), MetaPropertyOptions.Sealed);
        }

        public static MetaProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata metadata)
        {
            if (string.IsNullOrEmpty(name))
                throw new MetaPropertyException(Res.ExceptionPropertyNameNull);

            return MetaProperty.CreateAttached(name, propertyType, ownerType, metadata, MetaPropertyOptions.Sealed);
        }

        internal static MetaProperty RegisterDynamic(string name, Type propertyType, Type ownerType)
        {
            return MetaProperty.CreateDynamic(name, propertyType, ownerType,
                new PropertyMetadata(), MetaPropertyOptions.Sealed);
        }

        internal static MetaProperty RegisterDynamic(string name, Type propertyType, Type ownerType, PropertyMetadata metadata)
        {
            return MetaProperty.CreateDynamic(name, propertyType, ownerType, 
                metadata, MetaPropertyOptions.Sealed);
        }

        internal static MetaProperty RegisterDynamicScoped(string name, Type propertyType, Type ownerType, PropertyMetadata metadata)
        {
            return MetaProperty.CreateDynamicScoped(name, propertyType, ownerType, metadata,
                MetaPropertyOptions.Sealed);
        }

        #region Private Force Static Field Init

        /// <summary>
        /// Forces initialization of the static fields declared
        /// by a type, and any of its base class types.
        /// </summary>
        private static void ForceStaticFieldInit(Type type)
        {
            Debug.Assert(type != null);

            const BindingFlags attr = BindingFlags.Static |
                                      BindingFlags.Public |
                                      BindingFlags.DeclaredOnly |
                                      BindingFlags.NonPublic;
            var t = type;
            while (t != null)
            {
                var fields = t.GetFields(attr);
                if (fields.Length > 0)
                    fields[0].GetValue(null);
                t = t.BaseType;
            }
        }

        #endregion
    }
}